Spring Security 프로젝트 설정 10 - 권한 설정

✒️ 2025-05-28 14:28 내용 수정



역할과 권한 설정

참고 자료 : Bouali Ali's Spring boot 3 & Spring security 6 - Roles and Permissions Based Authorization Explained!

  1. HttpSecurity 인스턴스로 HTTP 요청에 대한 기본적인 권한 설정을 할 수 있다.
    • HttpSecurity로 설정 시 장점
      • 한 곳에서 Security 설정을 관리할 때 효과적이다.
      • 명확하고 직관적인 URL과 ROLE의 매핑 관계를 사용하는 경우에 유용하다.
      • Security 전체에 적용할 global 설정 및 모든 엔드포인트 설정을 지정할 때 사용한다.
      • 전체적인 Security 설정을 확인할 때 편리하다.
DSL 설명
permitAll 권한이 필요없는 public 엔드포인트
denyAll 어떤 상황에서든 접근 불가능한 엔드포인트
hasAuthority 접근하려면 Authentication이 주어진 값과 일치하는 GrantedAuthority를 가지고 있어야 함
hasRole hasAuthority의 단축형으로 ROLE_ 접두사나 기본 접두사를 사용
hasAnyAuthority 접근하려면 Authentication이 주어진 값들 중에 일치하는 GrantedAuthority를 가지고 있어야 함
hasAnyRole hasAnyAuthority의 단축형으로 ROLE_ 접두사나 기본 접두사를 사용
access AuthorizationManager가 접근을 결정
import static jakarta.servlet.DispatcherType.*;

import static org.springframework.security.authorization.AuthorizationManagers.allOf;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
	http
		// ...
		.authorizeHttpRequests(authorize -> authorize
            .dispatcherTypeMatchers(FORWARD, ERROR).permitAll()
			.requestMatchers("/static/**", "/signup", "/about").permitAll()
			.requestMatchers("/admin/**").hasRole("ADMIN")
			.requestMatchers("/db/**").access(allOf(hasAuthority("db"), hasRole("ADMIN")))
			.anyRequest().denyAll()
		);

	return http.build();
}
  1. HttpSecurity에서 권한 설정했던 내용을 Controller에서 @PreAuthorize를 통해 설정할 수 있다.
    • @PreAuthorize("hasRole('ADMIN')")으로 접근 가능한 역할을 지정한다.
    • 각 메소드에 @PreAuthorize("hasAuthority('admin:read')")로 메소드 접근에 필요한 권한을 설정한다.
    • SecurityConfig에 @EnableMethodSecurity를 추가한다.
    • @PreAuthorize으로 설정 시 장점
      • Security 설정을 구성 및 재구성 시 전체를 바꿀 필요 없이 Method에서 Annotation을 사용하여 관리할 수 있다.
      • 메소드 별로 관리할 때 편하다.
      • 복잡한 권한 설정을 해야 하는 경우 Annotation 기반으로 설정하는 것이 더 편하다.
        • 계층 구조 형태의 역할 및 권한을 사용할 때와 custom 권한을 사용할 때 효과적이다.
      • 가독성 측면에서도 HttpSecurity에서 지정하는 것 보다 좋다.
@Controller
@PreAuthorize("hasRole('ADMIN')")
public class TestController {
	@GetMapping
	@PreAuthorize("hasAuthority('admin:read')")
	public String endpoint() {
		return "GET endpoint";
	}
}
@Configuration  
@EnableWebSecurity  
@EnableMethodSecurity // @PreAuthorize를 사용하기 위해 필요  
public class SecurityConfig {
	// ...
}

테스트용 Controller 추가

  1. AdminController : ADMIN 역할을 가진 사용자만 접근 가능한 Controller다.
package com.example.security.auth;

import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

// ADMIN만 접근 가능
@RestController
@RequestMapping("/api/v1/admin")
@RequiredArgsConstructor
public class AdminController {

    @GetMapping
    public String get() {
        return "GET:: admin controller";
    }

    @PostMapping
    public String post() {
        return "POST:: admin controller";
    }

    @PutMapping
    public String put() {
        return "PUT:: admin controller";
    }

    @DeleteMapping
    public String delete() {
        return "DELETE:: admin controller";
    }
}

  1. ManagementController : ADMINMANAGER 역할을 가진 사용자만 접근 가능한 Controller다.
package com.example.security.auth;  
  
import lombok.RequiredArgsConstructor;  
import org.springframework.web.bind.annotation.*;  
  
// MANANGER와 ADMIN만 접근 가능  
@RestController  
@RequestMapping("/api/v1/management")  
@RequiredArgsConstructor  
public class ManagementController {  
    @GetMapping  
    public String get() {  
        return "GET:: management controller";  
    }  
  
    @PostMapping  
    public String post() {  
        return "POST:: management controller";  
    }  
  
    @PutMapping  
    public String put() {  
        return "PUT:: management controller";  
    }  
  
    @DeleteMapping  
    public String delete() {  
        return "DELETE:: management controller";  
    }  
}

권한 관련 클래스

  1. Permission enum 클래스 추가
    • 사용자의 역할 별 동작을 저장한 enum 클래스로, 특정 권한의 사용자가 어떤 동작을 할 수 있는지 CRUD에 따라 지정하였다.
    • 예를 들어 ADMIN 사용자가 특정 자원을 볼 수 있도록 하는 권한은 ADMIN_READ("admin:read")로 설정한다.
    • Lombok의 @Getter를 사용하여 이 권한 목록을 가져오도록 한다.
package com.example.security.user;  
  
import lombok.Getter;  
import lombok.RequiredArgsConstructor;  
  
@RequiredArgsConstructor  
public enum Permission {  
  
    ADMIN_READ("admin:read"),  
    ADMIN_UPDATE("admin:update"),  
    ADMIN_CREATE("admin:create"),  
    ADMIN_DELETE("admin:delete"),  
  
    MANAGER_READ("manager:read"),  
    MANAGER_UPDATE("manager:update"),  
    MANAGER_CREATE("manager:create"),  
    MANAGER_DELETE("manager:delete");  
  
    @Getter  
    private final String permission;  
}
  1. Role 클래스 수정
    • 기존의 USER, ADMIN 역할에서 테스트를 위해 MANAGER도 추가한다.
    • 각각의 역할에 Set으로 값을 주며, 여기에는 Permission에서 지정한 상수들을 입력한다.
    • ADMINADMIN의 권한과 MANAGER의 권한을 모두 가지고, MANAGERMANAGER의 권한만 가지며, 사용자는 아무 권한이 없다.
package com.example.security.user;  
  
import lombok.Getter;  
import lombok.RequiredArgsConstructor;  
import org.springframework.security.core.authority.SimpleGrantedAuthority;  
  
import java.util.Collections;  
import java.util.List;  
import java.util.Set;  
import java.util.stream.Collectors;  
  
@RequiredArgsConstructor  
public enum Role {  
    USER(Collections.emptySet()),  
    ADMIN(  
		Set.of(  
			Permission.ADMIN_READ,  
			Permission.ADMIN_UPDATE,  
			Permission.ADMIN_CREATE,  
			Permission.ADMIN_DELETE,  
			Permission.MANAGER_READ,  
			Permission.MANAGER_UPDATE,  
			Permission.MANAGER_CREATE,  
			Permission.MANAGER_DELETE  
		)  
    ),  
    MANAGER(  
		Set.of(  
			Permission.MANAGER_READ,  
			Permission.MANAGER_UPDATE,  
			Permission.MANAGER_CREATE,  
			Permission.MANAGER_DELETE  
		)  
    );  
  
    @Getter  
    // 중복 없이 권한 정보 가져오기  
    private final Set<Permission> permissions;  
  
    // Authorities 가져오기  
    // user에 getAuthorities에서도 사용  
    public List<SimpleGrantedAuthority> getAuthorities() {  
        var authorities = getPermissions()  
			.stream()  
			// spring에서 role = authorities
			.map(permission -> 
				new SimpleGrantedAuthority(permission.getPermission())
			)  
			.collect(Collectors.toList());  
          
        // prefix로 ROLE_을 추가한 권한을 마지막에 추가  
        authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));  
        return authorities;  
    }  
}
// Authorities 가져오기  
// user에 getAuthorities에서도 사용  
public List<SimpleGrantedAuthority> getAuthorities() {  
	var authorities = getPermissions()  
			.stream()  
			// spring에서 role = authorities
			.map(permission -> new SimpleGrantedAuthority(permission.getPermission()))  
			.collect(Collectors.toList());  
	  
	// prefix로 ROLE_을 추가한 역할을 마지막에 추가  
	authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));  
	return authorities;  
}  
@Bean static GrantedAuthorityDefaults grantedAuthorityDefaults() { 
	return new GrantedAuthorityDefaults("MYPREFIX_"); 
}

spring_security_authorities 6.png

.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/admin").hasAuthority("ROLE_ADMIN")
  1. User 클래스 수정
    • User 클래스에서 사용자의 권한을 가져오는 getAuthorities()RolegetAuthorities()를 호출하도록 수정한다.
package com.example.security.user;  
  
import jakarta.persistence.*;  
import lombok.AllArgsConstructor;  
import lombok.Builder;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
import org.springframework.security.core.GrantedAuthority;  
import org.springframework.security.core.authority.SimpleGrantedAuthority;  
import org.springframework.security.core.userdetails.UserDetails;  
  
import java.util.Collection;  
import java.util.List;  
  
@Data  
@Entity // Entity임을 명시  
@Builder // for Object building  
@NoArgsConstructor  
@AllArgsConstructor  
@Table(name = "user") // DB에 테이블 이름 지정  
public class User implements UserDetails {  
    // Spring Security의 UserDetails  
    @Id // id로 지정  
    @GeneratedValue(strategy = GenerationType.AUTO)  
    private Long id;  
    private String firstname;  
    private String lastname;  
    private String email;  
    private String password;  
  
    @Enumerated(EnumType.STRING) // Role이 Enum임을 명시  
    // EnumType.STRING은 String value 순으로 정렬  
    private Role role;  
  
    @Override  
    public Collection<? extends GrantedAuthority> getAuthorities() {  
        // 권한 List를 반환
        return role.getAuthorities();  
    }  

	//... 생략
}

SecurityConfig 수정

package com.example.security.config;  
  
import com.example.security.user.Permission;  
import com.example.security.user.Role;  
import lombok.RequiredArgsConstructor;  
// ... 생략
  
import java.util.Collections;  
  
@Configuration  
@EnableWebSecurity  
@RequiredArgsConstructor  
public class SecurityConfig {  
  
    private final JwtAuthenticationFilter jwtAuthFilter;  
    private final AuthenticationProvider authenticationProvider;  
    private final CustomLogoutHandler customLogoutHandler;  

	// 가시성을 위한 static 처리
    private static final Role ADMIN = Role.ADMIN;  
    private static final Role MANAGER = Role.MANAGER;
    
    @Bean  
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{  
  
        http  
			// session stateless로 인해 꺼 둠    
			.csrf((auth)->auth.disable())  
  
			.authorizeRequests()  

			// 요청 제어  
			.requestMatchers("/api/v1/auth/**") // 나열된 요청들은    
			.permitAll() // 모두 허용  

			// 권한이 필요한 요청 설정
			// ManagementController
			.requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name(), MANAGER.name())  

			.requestMatchers(HttpMethod.GET, "/api/v1/management/**").hasAnyAuthority(
				Permission.ADMIN_READ.name(), Permission.MANAGER_READ.name()
			)  
			.requestMatchers(HttpMethod.POST, "/api/v1/management/**").hasAnyAuthority(
				Permission.ADMIN_CREATE.name(), Permission.MANAGER_CREATE.name()
			)  
			.requestMatchers(HttpMethod.PUT, "/api/v1/management/**").hasAnyAuthority(
				Permission.ADMIN_UPDATE.name(), Permission.MANAGER_UPDATE.name()
			)  
			.requestMatchers(HttpMethod.DELETE, "/api/v1/management/**").hasAnyAuthority(
				Permission.ADMIN_DELETE.name(), Permission.MANAGER_DELETE.name()
			)  

			// AdminController
			.requestMatchers("/api/v1/admin").hasAnyRole(ADMIN.name())  

			.requestMatchers(HttpMethod.GET, "/api/v1/admin/**").hasAnyAuthority(
				Permission.ADMIN_READ.name()
			)  
			.requestMatchers(HttpMethod.POST, "/api/v1/admin/**").hasAnyAuthority(
				Permission.ADMIN_CREATE.name()
			)  
			.requestMatchers(HttpMethod.PUT, "/api/v1/admin/**").hasAnyAuthority(
				Permission.ADMIN_UPDATE.name()
			)  
			.requestMatchers(HttpMethod.DELETE, "/api/v1/admin/**").hasAnyAuthority(
				Permission.ADMIN_DELETE.name()
			)  

			.anyRequest() // 그 외의 모든 요청은    
			.authenticated() // 인증 필요    
			.and()  
			.sessionManagement((session)->  
					session // session state는 저장되면 안되므로 stateless로 설정    
			.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
			.authenticationProvider(authenticationProvider)  
			.addFilterBefore(jwtAuthFilter,  
					UsernamePasswordAuthenticationFilter.class); // jwt 필터 가동    

		// ... 생략
  
        return http.build();  
    }  

	// ... 생략
}

Test

  1. Application을 실행한 후 콘솔에 출력되는 Admin의 Access Token을 복사한다.

spring_security_authorities 1.png

  1. POSTMAN에 접속하고, Admin의 Access Token을 복사하여 Bearder Token에 넣은 후 http://localhost:port/api/v1/admin으로 GET요청을 보내면 status=200과 함께 Controller에서 지정한 String이 출력 된다.

spring_security_authorities 2.png

  1. Admin의 Access Token을 그대로 사용하여 http://localhost:port/api/v1/management로 GET 요청을 보내도 status=200과 함께 Controller에서 지정한 String이 출력 된다.

spring_security_authorities 3.png

  1. 이번엔 Manager의 Access Token을 사용하여 http://localhost:port/api/v1/management로 GET 요청을 보내 결과를 확인하면 status=200과 함께 String이 출력 된다.

spring_security_authorities 4.png

  1. Manager의 Access Token으로 http://localhost:port/api/v1/admin에 GET 요청을 보내면 status=403이 뜨며 접근이 제한된다.

spring_security_authorities 5.png


HttpSecurity 대신 @PreAuthorize 사용

package com.example.security.auth;  
  
import lombok.RequiredArgsConstructor;  
import org.springframework.security.access.prepost.PreAuthorize;  
import org.springframework.web.bind.annotation.*;  
  
// ADMIN만 접근 가능  
@RestController  
@RequestMapping("/api/v1/admin")  
@PreAuthorize("hasRole('ADMIN')") // ADMIN만 접근 가능  
@RequiredArgsConstructor  
public class AdminController {  
  
    @GetMapping  
    @PreAuthorize("hasAuthority('admin:read')")  
    public String get() {  
        return "GET:: admin controller";  
    }  
  
    @PostMapping  
    @PreAuthorize("hasAuthority('admin:create')")  
    public String post() {  
        return "POST:: admin controller";  
    }  
  
    @PutMapping  
    @PreAuthorize("hasAuthority('admin:update')")  
    public String put() {  
        return "PUT:: admin controller";  
    }  
  
    @DeleteMapping  
    @PreAuthorize("hasAuthority('admin:delete')")  
    public String delete() {  
        return "DELETE:: admin controller";  
    }  
}
package com.example.security.config;  

// ... 생략
  
import java.util.Collections;  
  
@Configuration  
@EnableWebSecurity  
@RequiredArgsConstructor  
@EnableMethodSecurity // @PreAuthorize를 사용하기 위해 필요  
// 최신 버전에선 prePostEnabled = true가 기본 설정이나  
// 구버전에선 prePostEnabled = false가 기본 설정  
// 구버전에선 @EnableGlobalMethodSecurity(prePostEnabled = true)로 사용
public class SecurityConfig {  
  
	// ... 생략
    @Bean  
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{  
  
        http  
			// session stateless로 인해 꺼 둠    
			.csrf((auth)->auth.disable())  
  
			.authorizeRequests()  

			// 요청 제어  
			.requestMatchers("/api/v1/auth/**") // 나열된 요청들은    
			.permitAll() // 모두 허용  
  
			// ... 생략

			// AdminController  
			// 주석 부분의 동작을 Annotation으로 똑같이 구현 가능  
//          .requestMatchers("/api/v1/admin").hasAnyRole(ADMIN.name())  
//  
//           .requestMatchers(HttpMethod.GET, "/api/v1/admin/**").hasAnyAuthority(Permission.ADMIN_READ.name())  
//           .requestMatchers(HttpMethod.POST, "/api/v1/admin/**").hasAnyAuthority(Permission.ADMIN_CREATE.name())  
//           .requestMatchers(HttpMethod.PUT, "/api/v1/admin/**").hasAnyAuthority(Permission.ADMIN_UPDATE.name())  
//           .requestMatchers(HttpMethod.DELETE, "/api/v1/admin/**").hasAnyAuthority(Permission.ADMIN_DELETE.name())  
  
	// ... 생략 
  
        return http.build();  
    }  

	// ... 생략
}